/**
 * @file net.c
 */

#define _BSD_SOURCE // avoid compilation warnings which appear when used in combination with -std=c99

#include <string.h>
#include <errno.h>
#include <stdlib.h>

#include <unistd.h>
#include <fcntl.h>
//#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
//#include <netinet/in.h>
#include <arpa/inet.h>

#include "net.h"
#include "common.h"

int socket_create(const Node *node, int type, int port)
{
	if(!node)
		return P2P_ERROR;

	int sock;
	struct sockaddr_in addr;

	/* Create socket */
	verbose(node, VSYSCL, "socket()\n");
	if((sock = socket(AF_INET, type, 0)) == -1) {
		perror("socket");
		return P2P_ERROR;
	}

	/* Allow bind() to succeed even when the socket is in TIMEWAIT state */
	verbose(node, VSYSCL, "setsockopt()\n");
	int yes = 1;
	if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) {
		perror("setsockopt()");
		close(sock);
		return P2P_ERROR;
	}

	/* Put socket in nonblocking mode */
	verbose(node, VSYSCL, "fcntl()\n");
	if(fcntl(sock, F_SETFL, O_NONBLOCK) == -1) {
		perror("fcntl()\n");
		close(sock);
		return P2P_ERROR;
	}

	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = htonl(INADDR_ANY);
	addr.sin_port = htons(port);

	/* Bind the socket to a local address:port */
	verbose(node, VSYSCL, "bind()\n");
	if(bind(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
		perror("bind");
		close(sock);
		return P2P_ERROR;
	}

	return sock;
}

int socket_close(const Node *node, int sock)
{
	verbose(node, VSYSCL, "close()\n");
	if(close(sock) == -1) {
		perror("close");
		return P2P_ERROR;
	}
	return P2P_OK;
}

/** 
 * @internal
 *
 * @param node
 * @param dst
 *
 * @return 
 */
int socket_create_dst(const Node *node, int type, const char *dst_ip, int dst_port, int timeout)
{
	/* This will be an outbound connection, so any port will do */
	int sock;
	if((sock = socket_create(node, type, 0)) == P2P_ERROR)
		return P2P_ERROR;

	/* Build destination address */
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	if(inet_aton(dst_ip, &addr.sin_addr) == 0) {
		perror("inet_aton");
		socket_close(node, sock);
		return P2P_ERROR;
	}
	addr.sin_port = htons(dst_port);

	/* Connect the local socket to the remote address dst_ip:dst_port
	 * The socket is supposed to have O_NONBLOCK */
	verbose(node, VSYSCL, "connect()\n");
	if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0)
		return sock; // connection succeeded right away
	else if(errno != EINPROGRESS) {
		perror("connect()");
		socket_close(node, sock);
		return P2P_ERROR;
	}

	/* Wait for connect() to succeed or time out */
	fd_set writefds;
	int ready;
	struct timeval tv;
	while(1) {
		FD_ZERO(&writefds);
		FD_SET(sock, &writefds);
		tv.tv_sec = timeout;
		tv.tv_usec = 0;

		/* Start timer */
		ready = select(sock+1, NULL, &writefds, NULL, &tv);
		if(ready == 0) {
			perror("connect() timed out\n");
			socket_close(node, sock);
			return P2P_ERROR;
		} else if(ready == -1 && errno != EINTR) {
			perror("select()");
			socket_close(node, sock);
			return P2P_ERROR;
		} else if(ready == -1 && errno == EINTR)
			continue; // we were interrupted, retry

		/* connect() has a result */
		int so_error;
		socklen_t len = sizeof(so_error);
		/* Determine the outcome of connect() */
		if(getsockopt(sock, SOL_SOCKET, SO_ERROR, &so_error, &len) == -1) {
			perror("getsockopt()");
			socket_close(node, sock);
			return P2P_ERROR;
		}
		if(so_error != 0) {
			fprintf(stderr, "connect(): %s\n", strerror(so_error));
			socket_close(node, sock);
			return P2P_ERROR;
		} else break;
	}

	/* Everything went fine. We have a connection. */
	return sock;
}

int tcp_socket_create(const Node *node, const Addr *dst)
{
	//verbose(node, VDEBUG, "tcp_socket_create()\n");
	return socket_create_dst(node, SOCK_STREAM, addr_get_ip(dst), addr_get_tcp_port(dst), 5); //TODO change default timeout into a global variable which can eventually be set with a command line option
}

int udp_socket_create(const Node *node, const Addr *dst)
{
	//verbose(node, VDEBUG, "udp_socket_create()\n");
	return socket_create_dst(node, SOCK_DGRAM, addr_get_ip(dst), addr_get_udp_port(dst), 5); //TODO same as tcp_socket_create
}

/** 
 * @brief Read with timeout from a nonblocking file descriptor.
 *
 * @param node 
 * @param fd the file descriptor to read from
 * @param buf the buffer to write to
 * @param count number of bytes to read
 * @param timeout the timeout in seconds
 *
 * @return 
 * 	- P2P_OK if _all_ data was read in the given time
 * 	- P2P_ERROR otherwise
 *
 * readt() reads _all_ requested data from a file descriptor which has been marked nonblocking.
 */
int readt(const Node *node, int fd, char *buf, int count, int timeout)
{
	int r; // how much we have read so far
	int ret; // return code from read()
	fd_set readfds;
	int ready; // return code from select();
	struct timeval tv;
	for(r = 0; r  < count; ) {
		FD_ZERO(&readfds);
		FD_SET(fd, &readfds);
		tv.tv_sec = timeout;
		tv.tv_usec = 0;

		/* Start timer */
		ready = select(fd+1, &readfds, NULL, NULL, &tv);
		if(ready == 0) {
			verbose(node, VERROR, "tcp_read(): timed out\n");
			return P2P_ERROR;
		} else if(ready == -1 && errno != EINTR) {
			perror("select()");
			return P2P_ERROR;
		} else if(ready == -1 && errno == EINTR)
			continue;

		/* Read */
		ret = read(fd, buf+r, count-r);
		if(ret > 0)
			r += ret;
		else if(ret == 0) {
			verbose(node, VERROR, "tcp_read(): connection closed by peer.\n");
			return P2P_ERROR;
		} else if(errno != EAGAIN && errno != EINTR) {
			perror("read()");
			return P2P_ERROR;
		}
	}
	return P2P_OK;
}

/** 
 * @brief Write with timeout to a nonblocking file descriptor.
 *
 * @param node
 * @param fd the file descriptor to write to
 * @param buf the buffer to read from 
 * @param count number of bytes to write
 * @param timeout the timeout in seconds
 *
 * @return 
 * 	- P2P_OK if _all data was written in the given time
 * 	- P2P_ERROR otherwise
 *
 * writet() writes _all_ requested data to a file descriptor which has been marked nonblocking.
 */
int writet(const Node *node, int fd, char *buf, int count, int timeout)
{
	int w; // how much we have written so far
	int ret; // return code from write()
	fd_set writefds;
	int ready; // return code from select();
	struct timeval tv;
	for(w = 0; w < count; ) {
		FD_ZERO(&writefds);
		FD_SET(fd, &writefds);
		tv.tv_sec = timeout;
		tv.tv_usec = 0;
		/* Start timer */
		ready = select(fd+1, NULL, &writefds, NULL, &tv);
		if(ready == 0) {
			verbose(node, VERROR, "tcp_write(): timed out\n");
			return P2P_ERROR;
		} else if(ready == -1 && errno != EINTR) {
			perror("select()");
			return P2P_ERROR;
		} else if(ready == -1 && errno == EINTR)
			continue;
		/* Write */
		ret = write(fd, buf+w, count-w);
		if(ret > 0)
			w += ret;
		else if(ret == 0)
			return P2P_ERROR;
		//FIXME Don't forget to catch EPIPE
		else if(errno != EAGAIN && errno != EINTR) {
			perror("write()");
			return P2P_ERROR;
		}
	}
	return P2P_OK;
}
